package ch.holzi.paperracers.view;

import java.util.ArrayList;
import java.util.Collections;
import java.util.LinkedList;

import ch.holzi.paperracers.util.Line;
import ch.holzi.paperracers.util.Vector;
import android.graphics.Canvas;
import android.graphics.Color;
import android.graphics.DashPathEffect;
import android.graphics.Paint;
import android.graphics.Path;
import android.graphics.PointF;
import android.util.Log;
import android.util.Pair;

public class TrackSegment
{
	private Path p;
	private TrackPoint startPoint;
	private TrackPoint controlPoint;
	private TrackPoint endPoint;
	private Paint paint;
		
	private LinkedList<TrackSegment> neighborTrack;
	private TrackSegment previous, next;
	
	private Paint p1 = new Paint();
	private Paint p2 = new Paint();
		
	public static final String LOG_TAG = "TrackSegment";
	
	// for internal calculation
	private PointF[] scalePoint = new PointF[4];
	
	enum Style
	{
		FULL,
		DOTTED,
		DASHED,
	}
	
	enum PointType
	{
		START,
		CONTROL,
		END
	}
		
	public TrackSegment(float startX, float startY, LinkedList<TrackSegment> neighbor)
	{
		p = new Path();
		paint = new Paint();
		paint.setColor(Color.BLACK);
		paint.setStyle(Paint.Style.STROKE);
		
		p1.setColor(Color.RED);
		
		p2.setColor(Color.BLUE);
		p2.setStrokeWidth(3);
		
		startPoint = new TrackPoint(startX, startY);
		controlPoint = new TrackPoint();
		endPoint = new TrackPoint();
		
		scalePoint[0] = new PointF();
		scalePoint[1] = new PointF();
		scalePoint[2] = new PointF();
		scalePoint[3] = new PointF();
				
		neighborTrack = neighbor;
		previous = null;
		next = null;
		initPath();
	}
	
	public TrackSegment(float startX, float startY)
	{
		p = new Path();
		paint = new Paint();
		paint.setColor(Color.BLACK);
		paint.setStyle(Paint.Style.STROKE);
		
		p1.setColor(Color.RED);
		
		p2.setColor(Color.BLUE);
		p2.setStrokeWidth(3);
		
		startPoint = new TrackPoint(startX, startY);
		controlPoint = new TrackPoint();
		endPoint = new TrackPoint();
		
		scalePoint[0] = new PointF();
		scalePoint[1] = new PointF();
		scalePoint[2] = new PointF();
		scalePoint[3] = new PointF();
				
		neighborTrack = null;
		previous = null;
		next = null;
		initPath();
	}
	
	public TrackSegment(TrackSegment s)
	{
		p = new Path();
		paint = new Paint();
		paint.setColor(s.paint.getColor());
		paint.setStyle(Paint.Style.STROKE);
		paint.setPathEffect(s.paint.getPathEffect());
		
		p1.setColor(Color.RED);
		
		startPoint = new TrackPoint(s.getStartPoint().x, s.getStartPoint().y, s.getStartPoint().getDistance());
		controlPoint = new TrackPoint(s.getControlPoint().x, s.getControlPoint().y, s.getControlPoint().getDistance());
		endPoint = new TrackPoint(s.getEndPoint().x, s.getEndPoint().y, s.getEndPoint().getDistance());
		scalePoint[0] = new PointF(s.getScalePoint(0).x, s.getScalePoint(0).y);
		scalePoint[1] = new PointF(s.getScalePoint(1).x, s.getScalePoint(1).y);
		scalePoint[2] = new PointF(s.getScalePoint(2).x, s.getScalePoint(2).y);
		scalePoint[3] = new PointF(s.getScalePoint(3).x, s.getScalePoint(3).y);
				
		neighborTrack = s.getNeighborTrack();
		previous = s.getPrevious();
		next = s.getNext();
		initPath();
		createPath();
	}
	
	void setColor(int color, Style s)
	{
		paint.setColor(color);
		if (s == Style.DOTTED)
		{
			paint.setPathEffect(new DashPathEffect(new float[] {2,5}, 0));
			paint.setStrokeWidth(2);
		}
		else if (s == Style.DASHED)
		{
			paint.setPathEffect(new DashPathEffect(new float[] {10,20}, 0));
			paint.setStrokeWidth(0);
		}
		else if (s == Style.FULL)
		{
			paint.setPathEffect(null);
			paint.setStrokeWidth(0);
		}
	}
	
	public void onDraw(Canvas c)
	{
		c.drawPath(p, paint);

		c.drawLine(startPoint.x, startPoint.y, controlPoint.x, controlPoint.y, p1);
		c.drawLine(endPoint.x, endPoint.y, controlPoint.x, controlPoint.y, p1);
		
	//	c.drawPoint(startPoint.x, startPoint.y, p2);
	//	c.drawPoint(endPoint.x, endPoint.y, p2);
	//	c.drawPoint(controlPoint.x, controlPoint.y, p2);
	}
	
	
	public void setPath(float controlX, float controlY, float endX, float endY)
	{
		controlPoint.x = controlX;
		controlPoint.y = controlY;
		
		endPoint.x = endX;
		endPoint.y = endY;
				
		createPath();
	}
	
	public void setNeighborTrack(LinkedList<TrackSegment> neighbor)
	{
		this.neighborTrack = neighbor;
	}
	
	public void setPrevious(TrackSegment s)
	{
		previous = s;
	}
	
	public void setNext(TrackSegment s)
	{
		next = s;
	}
	
	public TrackSegment getPrevious()
	{
		return previous;
	}
	
	
	public TrackSegment getNext()
	{
		return next;
	}

	
	LinkedList<TrackSegment> getNeighborTrack()
	{
		return neighborTrack;
	}
	
	Path getPath()
	{
		return p;
	}
	
	TrackPoint getStartPoint()
	{
		return startPoint;
	}
	
	TrackPoint getControlPoint()
	{
		return controlPoint;
	}
	
	TrackPoint getEndPoint()
	{
		return endPoint;
	}
	
	PointF getScalePoint(int i)
	{
		if (i < 0 || i > 3)
		{
			return null;
		}
		else
		{
			return scalePoint[i];
		}
	}

	
	/**
	 * calcuate the "scaled" (offsetted) tangents of the segment by:
	 * - get orthogonal, normalized vector to tangent 1 and 2
	 * - scale the orthogonal, normalized vector by the given scale
	 * - add this vector to the lines which define the tangents --> startPoint, controlPoint, endPoint
	 * --> new controlPoint can already be retrieved from intersection of the offsetted tangents
	 * @param scale
	 */
	void scale(float scale)
	{
		Vector tangent1 = new Vector(controlPoint, startPoint);
		Vector tangent2 = new Vector(endPoint, controlPoint);
		
		Vector orthoToTangent1 = tangent1.orthogonal(scale > 0).normalize().scale(Math.abs(scale));
		Vector orthoToTangent2 = tangent2.orthogonal(scale > 0).normalize().scale(Math.abs(scale));
		
		scalePoint[0].x = startPoint.x + orthoToTangent1.getX();
		scalePoint[0].y = startPoint.y + orthoToTangent1.getY();
		
		scalePoint[1].x = controlPoint.x + orthoToTangent1.getX();
		scalePoint[1].y = controlPoint.y + orthoToTangent1.getY();
		
		scalePoint[2].x = controlPoint.x + orthoToTangent2.getX();
		scalePoint[2].y = controlPoint.y + orthoToTangent2.getY();
		
		scalePoint[3].x = endPoint.x + orthoToTangent2.getX();
		scalePoint[3].y = endPoint.y + orthoToTangent2.getY();
		
		getIntersection(controlPoint, scalePoint[0], scalePoint[1], scalePoint[2], scalePoint[3]);
		
		Log.d(LOG_TAG, "controlPoint.x= "+controlPoint.x);
		Log.d(LOG_TAG, "controlPoint.y= "+controlPoint.y);
	}
	
	/**
	 * get new startPoint and endPoint by calculating the intersections of the previous segment's tangent with this one resp.
	 * the next segment's tangent with this one. In case these tangents are parallel the intermediate helper point of the 
	 * offsetted tangent is used as new start-/endPoint.
	 * @param previous segment before this one
	 * @param next segment after this one
	 */
	void scale(TrackSegment previous, TrackSegment next)
	{
		getIntersection(startPoint, previous.getScalePoint(2), previous.getScalePoint(3), scalePoint[0], scalePoint[1]);
		getIntersection(endPoint, scalePoint[2], scalePoint[3], next.getScalePoint(0), next.getScalePoint(1));


		Log.d(LOG_TAG, "startPoint.x= "+startPoint.x);
		Log.d(LOG_TAG, "startPoint.y= "+startPoint.y);		
		Log.d(LOG_TAG, "endPoint.x= "+endPoint.x);
		Log.d(LOG_TAG, "endPoint.y= "+endPoint.y);
		
		initPath();
		createPath();
	}
	
	/**
	 * Set the intersection point of 2 line segments defined by points p0 --> p1 resp. p2 --> p3
	 * @param intersection
	 * @param p0 start of line segment 1
	 * @param p1 end of line segment 1
	 * @param p2 start of line segment 2
	 * @param p3 end of line segment 2
	 */
	void getIntersection(PointF intersection, PointF p0, PointF p1, PointF p2, PointF p3)
	{
		PointF a = new PointF(p1.x - p0.x, p1.y - p0.y);
		PointF b = new PointF(p2.x - p3.x, p2.y - p3.y);
		PointF c = new PointF(p2.x - p0.x, p2.y - p0.y);
		
		float denom = (a.y * b.x - a.x * b.y);
		
		if (denom == 0)
		{
			intersection.x = p2.x;
			intersection.y = p2.y;
		}
		else
		{
			float t = (b.x * c.y - b.y * c.x) / denom;
			
			intersection.x = p0.x + t * (p1.x - p0.x);
			intersection.y = p0.y + t * (p1.y - p0.y);
		}
	}
	
	/**
	 * Gets the intersection point - if any - of the 2 line segments defined by 
	 * p0 --> p1 and p2 --> p3
	 * @param p0 start point of line segment 1
	 * @param p1 end point of line segment 1
	 * @param p2 start point of line segment 2
	 * @param p3 end point of line segment 2
	 * @return intersection point of the 2 segments, or null if they don't intersect
	 */
	PointF getIntersectionSegment(PointF p0, PointF p1, PointF p2, PointF p3)
	{
		PointF intersection = new PointF();
		getIntersection(intersection, p0, p1, p2, p3);
		if (isPointInSegment(intersection, p0, p1))
		{
			return intersection;
		}
		else
		{
			return null;
		}
	}
	
	boolean isPointInSegment(PointF p, PointF p0, PointF p1)
	{
		return isPointNearSegment(p, p0, p1, 0);
	}
	
	boolean isPointNearSegment(PointF p, PointF p0, PointF p1, int tolerance)
	{
		Vector p0ToP1 = new Vector(p1, p0);
		Vector p0ToP = new Vector(p, p0);
		
		if (!p0ToP1.isParallel(p0ToP, tolerance))
		{
			return false;
		}
		// line segment is not vertical
		if (p0.x != p1.x)
		{
			if (p0.x <= p.x && p.x <= p1.x)
			{
				return true;
			}
			if (p0.x >= p.x && p.x >= p1.x)
			{
				return true;
			}
		}
		else
		{
			if (p0.y <= p.y && p.y <= p1.y)
			{
				return true;
			}
			if (p0.y >= p.y && p.y >= p1.y)
			{
				return true;
			}
		}
		return false;
	}
	
	float getPointToSegmentDistance(PointF p, PointF p0, PointF p1)
	{
		Vector v = new Vector(p1, p0);
		Vector v_norm = v.orthogonal(true).	normalize();
		Vector pToP0 = new Vector(p, p0);
		return pToP0.cross(v) * v.length() / (v.cross(v_norm));
	}
	
	void movePointByDistance(PointF p, float distance, Vector direction)
	{
		Vector moveBy = direction.scale(distance);
		p.x = p.x + moveBy.getX();
		p.y = p.y + moveBy.getY();
	}
	
	private Distance getDistance(PointF p)
	{
		ArrayList<Distance> distances = new ArrayList<Distance>();
		
		for (int i = 0; i < neighborTrack.size()-1; ++i)
		{			
			PointF neighborStartPoint = neighborTrack.get(i).getStartPoint();
			PointF neighborControlPoint = neighborTrack.get(i).getControlPoint();
			PointF neighborEndPoint = neighborTrack.get(i).getEndPoint();
			
			
			distances.add( new Distance( getPointToSegmentDistance(p, neighborStartPoint, neighborControlPoint), new Vector(neighborStartPoint, neighborControlPoint)) );
			distances.add( new Distance( getPointToSegmentDistance(p, neighborEndPoint, neighborControlPoint), new Vector(neighborEndPoint, neighborControlPoint)) );
		}
		
		return Collections.min(distances, new DistanceComparator());
	}
	
	public void setDistance()
	{
		startPoint.setDistance(getDistance(startPoint));
		controlPoint.setDistance(getDistance(controlPoint));
		endPoint.setDistance(getDistance(endPoint));
	}
	
	public void cleanup()
	{
		if (startPoint.getDistance().distance < 40 && previous.getEndPoint().getDistance().distance < 40)
		{
			movePointByDistance(startPoint, 48-40, startPoint.getDistance().direction);
			startPoint.getDistance().distance = 48;
		}
		else if (startPoint.getDistance().distance < 40 && previous.getEndPoint().getDistance().distance >= 40)
		{
			startPoint.x = previous.getEndPoint().x;
			startPoint.y = previous.getEndPoint().y;
			startPoint.getDistance().distance = previous.getEndPoint().getDistance().distance;
		}
		
		if (controlPoint.getDistance().distance < 40)
		{
			movePointByDistance(controlPoint, 48-40, controlPoint.getDistance().direction);
			controlPoint.getDistance().distance = 48;
		}
	
		
		if (endPoint.getDistance().distance < 40 && next.getStartPoint().getDistance().distance < 40)
		{
			movePointByDistance(endPoint, 48-20, endPoint.getDistance().direction);
			endPoint.getDistance().distance = 48;
		}
		else if (endPoint.getDistance().distance < 40 && next.getStartPoint().getDistance().distance >= 40)
		{
			endPoint.x = next.getStartPoint().x;
			endPoint.y = next.getStartPoint().y;
			endPoint.getDistance().distance = next.getStartPoint().getDistance().distance;
		}
	}
	
	
	public void cleanup(TrackSegment next)
	{
		if (!endPoint.equals(next.getStartPoint().x, next.getStartPoint().y))
		{
			endPoint.x = (endPoint.x + next.getStartPoint().x) / 2;
			endPoint.y = (endPoint.y + next.getStartPoint().y) / 2;
			
			next.getStartPoint().x = endPoint.x;
			next.getStartPoint().y = endPoint.y;
		}
		
		initPath();
		createPath();
	}
	
	
	public void cleanup(TrackSegment previous, TrackSegment next)
	{
		if (isPointInSegment(controlPoint, previous.getControlPoint(), previous.getEndPoint()) 
			|| isPointNearSegment(controlPoint, previous.getControlPoint(), previous.getEndPoint(), 5))
		{
			startPoint.x = controlPoint.x;
			startPoint.y = controlPoint.y;
			
			previous.getEndPoint().x = controlPoint.x;
			previous.getEndPoint().y = controlPoint.y;
			
			controlPoint.x = (startPoint.x + endPoint.x) / 2;
			controlPoint.y = (startPoint.y + endPoint.y) / 2;
		}
		if (isPointInSegment(controlPoint, next.getStartPoint(), next.getControlPoint()) 
			|| isPointNearSegment(controlPoint, next.getStartPoint(), next.getControlPoint(), 5))
		{
			endPoint.x = controlPoint.x;
			endPoint.y = controlPoint.y;
			
			next.getStartPoint().x = controlPoint.x;
			next.getStartPoint().y = controlPoint.y;
			
			controlPoint.x = (startPoint.x + endPoint.x) / 2;
			controlPoint.y = (startPoint.y + endPoint.y) / 2;
		}
		
		initPath();
		createPath();
	}
	
	public void cleanup(TrackSegment beforePrevious, TrackSegment previous, TrackSegment next, TrackSegment afterNext)
	{
		// check if this segment crosses the next one
		PointF intersection = getIntersectionSegment(controlPoint, endPoint, next.getControlPoint(), next.getEndPoint());
		// we have a crossing!
		if (intersection != null)
		{
			endPoint.x = intersection.x;
			endPoint.y = intersection.y;
			
			afterNext.getStartPoint().x = intersection.x;
			afterNext.getStartPoint().y = intersection.y;
			
			next.getStartPoint().x = endPoint.x;
			next.getStartPoint().y = endPoint.y;
			
			next.getEndPoint().x = afterNext.getStartPoint().x;
			next.getEndPoint().y = afterNext.getStartPoint().y;
			
			next.getControlPoint().x = (next.getStartPoint().x + next.getEndPoint().x) / 2;
			next.getControlPoint().y = (next.getStartPoint().y + next.getEndPoint().y) / 2;
		}
		
		// check if this segment crosses the after-next one
		intersection = getIntersectionSegment(controlPoint, endPoint, afterNext.getStartPoint(), afterNext.getControlPoint());
		// we have a crossing!
		if (intersection != null)
		{
			endPoint.x = intersection.x;
			endPoint.y = intersection.y;
			afterNext.getStartPoint().x = intersection.x;
			afterNext.getStartPoint().y = intersection.y;
			
			next.getStartPoint().x = endPoint.x;
			next.getStartPoint().y = endPoint.y;
			
			next.getEndPoint().x = afterNext.getStartPoint().x;
			next.getEndPoint().y = afterNext.getStartPoint().y;
			
			next.getControlPoint().x = (next.getStartPoint().x + next.getEndPoint().x) / 2;
			next.getControlPoint().y = (next.getStartPoint().y + next.getEndPoint().y) / 2;

		}
		
		initPath();
		createPath();
	}
	
	private void initPath()
	{
		p.reset();
		p.moveTo(startPoint.x, startPoint.y);
	}
	
	private void createPath()
	{
		if (isPointInSegment(controlPoint, startPoint, endPoint))
		{
			p.lineTo(endPoint.x, endPoint.y);
		}
		else
		{
			p.quadTo(controlPoint.x, controlPoint.y, endPoint.x, endPoint.y);
		}
	}
}

